﻿using System;
using System.IO;
using System.Reflection;
using Fadd.JSON.Tokens;
using Array=Fadd.JSON.Tokens.Array;
using Boolean=Fadd.JSON.Tokens.Boolean;
using Object=Fadd.JSON.Tokens.Object;
using String=Fadd.JSON.Tokens.String;

namespace Fadd.JSON
{
	/// <summary>
	/// Deserialize JSON to .Net objects.
	/// </summary>
	public class Deserializer
	{
		/// <summary>
		/// Deserialize
		/// </summary>
		/// <param name="stream">Stream to read objects from</param>
		/// <param name="type">Type to deserialize</param>
		/// <returns>Object if deserialization was successful.</returns>
		/// <exception cref="ArgumentNullException"><c>stream</c>  or <c>type</c> is null.</exception>
		/// <exception cref="ArgumentException">Stream is not readable.</exception>
		/// <exception cref="InvalidOperationException">If the JSON document is malformed or a type can't be identified.</exception>
		/// <exception cref="System.Reflection.TargetInvocationException">Something in the types constructor failed.</exception>
		/// <exception cref="System.TypeLoadException">Failed to load type</exception>
		public object Deserialize(Stream stream, Type type)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			if (type == null)
				throw new ArgumentNullException("type");
			if (!stream.CanRead)
				throw new ArgumentException("Stream is not readable.");

			object obj = Activator.CreateInstance(type);
			var reader = new JsonReader(stream);
			Object root = reader.ReadObject();
			if (root == null)
				throw new InvalidOperationException("Failed to read root object");

			DeserializeObject(type, root, obj);
			return obj;
		}

		/// <summary>
		/// Deserialize
		/// </summary>
		/// <param name="reader">Reader to read objects from</param>
		/// <param name="type">Type to deserialize</param>
		/// <returns>Object if deserialization was successful.</returns>
		/// <exception cref="ArgumentNullException"><c>stream</c>  or <c>type</c> is null.</exception>
		/// <exception cref="ArgumentException">Stream is not readable.</exception>
		/// <exception cref="InvalidOperationException">If the JSON document is malformed or a type can't be identified.</exception>
		/// <exception cref="System.Reflection.TargetInvocationException">Something in the types constructor failed.</exception>
		/// <exception cref="System.TypeLoadException">Failed to load type</exception>
		public object Deserialize(TextReader reader, Type type)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");
			if (type == null)
				throw new ArgumentNullException("type");

			object obj = Activator.CreateInstance(type);
			var jsonreader = new JsonReader(reader);
			Object root = jsonreader.ReadObject();
			if (root == null)
				throw new InvalidOperationException("Failed to read root object");

			DeserializeObject(type, root, obj);
			return obj;
		}

		/// <exception cref="InvalidOperationException">Unknown collection type.</exception>
		private void DeserializeObject(Type type, Object root, object obj)
		{
			if (type == null)
				throw new ArgumentNullException("type");
			if (obj == null)
				throw new ArgumentNullException("obj");

			foreach (PropertyInfo property in type.GetProperties())
			{
				IToken token = root.GetChild(property.Name);
				if (token == null)
					continue;

				// Handle array manually, since we can take advantage of Add methods.
				if (token is Array)
				{
					if (property.PropertyType.IsArray)
					{
						ProcessStandardArray(property, token, obj);
						continue;
					}

					if (!property.PropertyType.IsGenericType)
						throw new InvalidOperationException("Unknown collection type.");

					ProcessGenericList(type, property, token, obj);
				}
				else
				{
					if (property.IsSpecialName || property.GetIndexParameters().Length > 0)
						continue;

					if (property.CanWrite)
						AssignValue(root, property, token, obj);
				}
			}
		}

		private string GetTypeNameWithAssembly(Type type)
		{
			return type.FullName.Replace("[]", string.Empty) + ", " +
			       type.Assembly.GetName().Name;
		}

		/// <exception cref="InvalidOperationException">Currently only support regular generic lists.</exception>
		private void ProcessGenericList(Type type, PropertyInfo property, IToken token, object obj)
		{
			Type[] types = property.PropertyType.GetGenericArguments();
			if (types.Length != 1)
				throw new InvalidOperationException("Currently only support regular generic lists.");
			Type propertyType = types[0];

			object collectionObject = obj;
			//System.Collections.Generic.List`1[Fadd.JSON.Test.PhoneNumber]
			Type collectionType = Type.GetType("System.Collections.Generic.ICollection`1[[" + GetTypeNameWithAssembly(propertyType) + "]]");
			if (collectionType == null)
				throw new InvalidOperationException("Failed to find collection type for: " + propertyType.FullName + ", property: " + type.Name + "." + property.Name);

			MethodInfo methodInfo = type.GetMethod("Add", new[] {propertyType});
			if (methodInfo == null)
			{
				methodInfo = collectionType.GetMethod("Add", new[] {propertyType});
				if (methodInfo == null)
					throw new InvalidOperationException("Failed to add");
				collectionObject = property.GetValue(obj, null);
			}

			var array = (Array) token;
			foreach (IToken element1 in array)
			{
				object child = GetValue(element1, propertyType);
				methodInfo.Invoke(collectionObject, new[] {child});
			}
		}

		private void ProcessStandardArray(PropertyInfo property, IToken token, object obj)
		{
			MethodInfo assignMethod = property.GetSetMethod();
			if (assignMethod == null)
				return;

			var array = (Array) token;

			string typeName = property.PropertyType.FullName.Replace("[]", string.Empty) + "," + property.PropertyType.Assembly.GetName().Name;
			Type propertyType = Type.GetType(typeName);
			object arr = Activator.CreateInstance(property.PropertyType, new object[] {array.Items.Count});
			MethodInfo setMethod = property.PropertyType.GetMethod("SetValue", new[] {typeof (object), typeof (int)});

			int index = -1;
			foreach (IToken element1 in array)
			{
				object child = GetValue(element1, propertyType);
				setMethod.Invoke(arr, new[] {child, ++index});
			}

			assignMethod.Invoke(obj, new[] {arr});
		}

		private void AssignValue(Object root, PropertyInfo property, IToken token, object obj)
		{
			object value;
			if (property.PropertyType != typeof (object))
				value = GetValue(token, property.PropertyType);
			else
			{
				IToken typeSpecifier = root.GetChild("__" + property.Name);
				if (typeSpecifier == null)
					throw new InvalidOperationException("Failed to identify type for object: " + token);
				Type propertyType = Type.GetType((typeSpecifier).ToString());
				value = GetValue(token, propertyType);
			}

			if (value != null)
				property.SetValue(obj, value, null);
		}

		/// <exception cref="InvalidOperationException"><c>InvalidOperationException</c>.</exception>
		private object GetValue(IToken token, Type type)
		{
			if (token is String)
			{
				var str = (String) token;
				object value;
				if (type == typeof (DateTime))
					value = DateTime.Parse(str.ToString());
				else if (type.IsEnum)
				{
					try
					{
						value = Enum.Parse(type, str.ToString());
					}
					catch(ArgumentException err)
					{
						throw new InvalidOperationException("Could not convert enum string to " + type, err);
					}
				}
				else if (type.IsAssignableFrom(typeof (string)))
					value = str.ToString();
				else
					throw new InvalidOperationException("Could not convert string to " + type);

				return value;
			}
			if (token is Number)
			{
				var nr = (Number) token;
				object value = Convert.ChangeType(nr.Value, type);
				return value;
			}
			if (token is Object)
			{
				CreatingTypeEventArgs args = new CreatingTypeEventArgs(type);
				CreatingType(this, args);
				if (!args.CanInstantiate)
					return null;
				
				object child;
				if (args.Instance != null)
				{
					child = args.Instance;
					type = args.Instance.GetType();
				}
				else
					child =Activator.CreateInstance(type, null);

				DeserializeObject(type, (Object) token, child);
				return child;
			}
			if (token is Boolean)
			{
				return ((Boolean) token).Value;
			}

			throw new InvalidOperationException("Unknown type: " + token);
		}

		/// <summary>
		/// Called when a instance of a specific type is about to be created.
		/// </summary>
		public event EventHandler<CreatingTypeEventArgs> CreatingType = delegate { };
	}

	/// <summary>
	/// Event arguments for <see cref="Deserializer.CreatingType"/>.
	/// </summary>
	public class CreatingTypeEventArgs : EventArgs
	{
		/// <summary>
		/// Gets type to create
		/// </summary>
		public Type TypeToCreate { get; private set; }

		/// <summary>
		/// Created instance.
		/// </summary>
		public object Instance { get; set;}

		/// <summary>
		/// Gets or sets whether the type should get an instance.
		/// </summary>
		public bool CanInstantiate { get; set; }

		/// <summary>
		/// Initializes a new instance of the <see cref="CreatingTypeEventArgs"/> class.
		/// </summary>
		/// <param name="typeToCreate">The type to create.</param>
		public CreatingTypeEventArgs(Type typeToCreate)
		{
			TypeToCreate = typeToCreate;
			CanInstantiate = true;
		}
	}
}